Through the Galaxy with Microframeworks
The fastest way to modern Java Microservices
In the JVM area, Spring Boot and Eclipse MicroProfile have established themselves as quasi-standard frameworks for microservices development and have also contributed significantly to the success of the architecture pattern. However, these frameworks entail some overhead, since their core originates from the era before microservices and the cloud.
Therefore, young, lightweight JVM frameworks that have been designed from the ground up and fully for cloud applications have recently gained importance. In addition, frameworks that have existed for some time and are characterized by their lightweight nature are gaining new momentum in modern cloud operating scenarios. This trend can be summarized under the term JVM Microframeworks.
What are JVM microframeworks?
It is difficult to find a universal definition of what exactly characterizes a JVM microframework, as the range is simply too wide. Javalin, for example, is deliberately much more focused and reduced in its range of functions. Micronaut and Quarkus, on the other hand, offer considerably more features and position themselves more as full-stack frameworks.
Nevertheless, there are some basic characteristics that are found in all JVM microframeworks, although these characteristics may be more or less pronounced:
- Designed for the creation of cloud-native microservices
- core component: lightweight web framework
- fast startup times and low memory consumption
- Focus on simplicity and development speed
- GraalVM-friendly
Designed for the creation of cloud-native microservices.
Basically, JVM microframeworks are intended for the development of microservices landscapes. More specifically, they help in the creation of manageably sized, modular services that are usually operated in the cloud and are intended to be horizontally scalable there. Mostly, they are packaged as Fat/Uber JAR and only require a JVM to run. Usually, these artifacts are deployed in a (Docker) container and orchestrated by a cloud platform like Kubernetes.
Core component: lightweight web framework
In order to offer a service in the cloud, a server – such as Tomcat, Jetty or Netty – must be configured, started and the corresponding process kept running. This can then be used to offer REST APIs and deliver web content.
Fast startup times and low memory consumption
In many established frameworks, such as Spring, the resolution of framework logic relies heavily on reflection, runtime proxies, dynamic classloading, and classpath scanning. All of these functionalities are very powerful in their own right. However, they are also CPU and memory-intensive, which can have a negative impact on the startup time and memory footprint of an application.
The reason for this is that these frameworks originated when the optimization of long-running processes was still the measure of all things. Here, disadvantages in startup time and memory consumption were not yet so noticeable. In a cloud-native microservices architecture, however, in which instances are quickly scaled and redistributed, this now plays a significant role.
Microframeworks manage to implement their framework logic differently. How they achieve this is very framework-specific: Often, the configuration is more explicit, so that fewer are done by convention and thus have to be determined via reflection. Some frameworks also completely dispense with features that could only be implemented using reflection. Still other frameworks use an alternative compile approach to resolve dependencies at compile time rather than at startup and runtime.
However, all frameworks have one thing in common: they achieve significant improvements in startup time and memory consumption, which predestines them for cloud operation.
Focus on simplicity and development speed
Microframeworks clearly place the developer experience in the foreground: It becomes possible to create a (micro)service very quickly. Even getting started or learning the framework functionalities is not a major hurdle.
Some of the frameworks focus on core concepts in application development and deliberately have a smaller feature set than full-stack frameworks such as Spring or Grails.
However, as mentioned at the beginning, the bandwidth is particularly large: The spectrum ranges from frameworks that really limit themselves to the elementary and do without advanced features such as dependency injection, various database abstractions, or OAuth support, to those that offer virtually all common components.
GraalVM-friendly
GraalVM, Oracle’s new polyglot virtual machine, offers numerous innovations and optimizations.
The Substrate VM [1] is a particularly exciting component from the perspective of JVM-based programming languages such as Java, Kotlin, and Scala. It allows Ahead of Time (AoT) compilation to generate executable native binaries from JVM applications. Put simply, the Native Image Generator allows you to create an executable from a JAR and all of its dependencies that can be run directly – without a JVM. Application start time and memory consumption can be reduced again drastically, usually to milliseconds and a few megabytes. The use of such executables is interesting when the startup time is the decisive factor, for example, with only short-running serverless functions.
However, reflection, runtime proxies, and dynamic classloading present the AoT compilation with major challenges, some of which are still unsolvable at present. Since, as described before, JVM Microframeworks realize their logic to a large extent differently and manage to get along without reflection, etc., the creation of a native binary with the GraalVM turns out clearly more simply. In addition, some of the frameworks such as Quarkus are directly optimized for GraalVM. This means that they not only facilitate development, but also envisage the GraalVM as a possible target platform from the ground up.
Possible applications
When does it make sense to use a microframework? Or rather, when is it worthwhile not to rely on a classic framework for application development, but on one of the alternatives?
First and foremost, use it whenever you have a containerized microservices environment with dynamic load scenarios. In other words, when services need to start quickly. This may be required, when scaling due to a peak load, or when restarting in case of failover, or due to platform or infrastructure patches. This is especially relevant for services deployed via containers (such as Docker), since only limited resources are available in a container. Often, for example, only one CPU core and limited memory are available.
And even if there is enough memory in the cloud, it’s the sum that counts. With five running service instances, 300 megabytes of memory saved per instance add up to almost one and a half gigabytes that do not have to be used and do not have to be paid for.
Serverless functions are the second area of application. For these short-running processes, the cold start time, i.e. the time until the function is initially ready, is an even more decisive factor. Since the serverless payment model usually correlates directly with resource consumption, memory consumption is also a direct cost factor. This is why Micronaut, for example, provides its own AWS Lambda support for creating Functions [2]. With the aforementioned generation of executables by the GraalVM, it is possible to go one step further, so that Java applications in the serverless area can compete with functions written in Go.
The use of a microframework also lends itself to the construction of an API gateway. API gateways – also known as edge services – are a fundamental component of a microservices architecture. They form the central access point for all requests to the backend services [3]. As a result, an API gateway must scale at least as well as the services behind it. Startup times are critical to success. Vert.x and Ktor, for example, are reactive frameworks that are ideally suited for the fast routing of requests – the central task of an API gateway.
And last but not least, microframeworks can be useful when stubbing services. Let’s assume that a service is not available on a development stage that will be connected on the production stage. In this case, it can be helpful to mock the service that will be connected on the development stage. A service stub can be created quickly with a lightweight framework such as Javalin or http4k.
Risks and side effects
Of course, using a new technology also entails risks. Even if Quarkus, Helidon, and Micronaut are preparing to seriously compete with the top dog Spring Boot in the enterprise, they are relatively young frameworks. Consequently, they cannot provide the complete ecosystem of established frameworks that have been developed for years or decades. Other, more minimalist microframeworks will deliberately never have a fully comprehensive feature set. Framework documentation, code samples, and associated communities are currently catching up, but are understandably not at the level of established frameworks.
It is therefore advisable to roughly define the scope of the service to be created before selecting a framework: Are several components required and should Dependency Injection be usable out of the box? Are different database abstractions required? What authentication and authorization mechanisms need to be supported? Should messaging systems like RabbitMQ or Kafka be connected? Soft factors such as programming language know-how and a team’s willingness to innovate should not be neglected.
In general, the microservices architecture pattern helps to minimize the risk: A new framework can be used specifically in one service that is as isolated as possible from a technical point of view. This means that the risk to the overall system can be limited. If it turns out that the framework is not suitable, it only affects one service and not the entire microservices landscape. However, if the framework turns out to be suitable, the lessons learned can be applied to the other services.
Finally, a note about the GraalVM and the possibility to create executables with it. You should be aware that the “write once, run anywhere” paradigm of Java is left behind. Which means, among other things, that throughput optimizations are omitted, which the JVM achieves by means of combined runtime profiling and JIT compiler. In addition, the development roundtrip time is significantly longer, as it can take several minutes to build a native binary. After building the JAR, additional, time-consuming steps become necessary until the final artifact is available.
Conclusion
There is something happening in the framework market. JVM microservices code samples are deposited on GitHub [4].
There are numerous comparable microframeworks, such as Dropwizard [5], Jooby [6], Jodd [7], Pippo [8], Hexagon [9] or Ninja [10], to name just a few.
Whether this will lead to a replacement of the established Spring, Grails, and Eclipse MicroProfile remains to be seen. Currently, it looks more as if there is enough space on the market for all frameworks, especially since there cannot be one framework for all use cases.
One thing is already apparent, however: competition is good for business. For example, the Spring community has responded with several optimization strategies, some of which have already been incorporated into Spring Boot 2.2, as well as the functional Spring Fu and the still-experimental Spring Graal Native project [11].
These are definitely exciting times for framework users and developers. Having alternatives is certainly more advantageous than being limited to a few options. Or to put it in terms of Maslow’s hammer: “If all you have is a hammer, everything looks like a nail.” With the new selection of frameworks, the developer toolbox has become much more diverse.
Links & Literature
- https://www.graalvm.org/docs/reference-manual/native-image
- https://micronaut-projects.github.io/micronaut-aws/latest/guide/#lambda
- https://www.informatik-aktuell.de/entwicklung/methoden/api-gateways-eine-praktische-einfuehrung.html
- https://github.com/csh0711/jvm-microframeworks-hello-world
- https://www.dropwizard.io
- https://jooby.io
- https://jodd.org
- http://www.pippo.ro
- https://hexagonkt.com
- https://www.ninjaframework.org
- https://github.com/spring-projects/spring-framework/wiki/GraalVM-native-image-support